package org.internna.expensetracker.mvc;

import java.util.Map;
import java.util.Date;
import java.util.List;
import java.util.Locale;
import java.util.HashMap;
import java.util.TreeMap;
import java.util.TreeSet;
import java.util.Calendar;
import java.util.Currency;
import java.util.Collection;
import java.math.BigDecimal;
import javax.servlet.http.HttpServletRequest;

import org.internna.expensetracker.cache.CacheStore;
import org.internna.expensetracker.model.Category;
import org.internna.expensetracker.util.DateUtils;
import org.internna.expensetracker.model.Subcategory;
import org.internna.expensetracker.model.budget.Budget;
import org.internna.expensetracker.model.support.Interval;
import org.internna.expensetracker.model.support.NameValuePair;
import org.internna.expensetracker.model.AccountTransaction;
import org.internna.expensetracker.model.security.UserDetails;
import org.internna.expensetracker.model.support.InflowOutflow;
import org.springframework.ui.ModelMap;
import org.springframework.util.CollectionUtils;
import org.springframework.context.MessageSource;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;

@Controller
@RequestMapping("/financial/reporting/**")
public final class ReportingController extends AbstractTransactionController {

	@Autowired private CacheStore cache;
	@Autowired private MessageSource messageSource;
	@Autowired private WidgetController widgetController;

    @RequestMapping
    public String index() {
        return "reporting/index";
    }

    protected void setCache(CacheStore cache) {
		this.cache = cache;
	}

	@RequestMapping("inflows-outflows/{intervals}")
    public String monthlyInflowsOutflowsReport(@PathVariable final String intervals, final HttpServletRequest request, final ModelMap modelMap) {
    	Interval interval = getInterval(intervals);
    	modelMap.addAttribute("months", DateUtils.dates(interval.getNumberOfMonths() - 1));
    	List<AccountTransaction> transactions = getTransactions(interval, null, modelMap);
    	modelMap.addAttribute("data", calculateMonthlyInflowsOutflows(interval, transactions));
        return "reporting/monthly-inflows-outflows";
    }

    protected final Collection<InflowOutflow> calculateMonthlyInflowsOutflows(final Interval interval, final List<AccountTransaction> transactions) {
    	Map<String, InflowOutflow> data = new HashMap<String, InflowOutflow>();
    	if (!CollectionUtils.isEmpty(transactions)) {
    		for (AccountTransaction transaction : transactions) {
    			Locale locale = transaction.getAccount().getLocale();
    			String currency = transaction.getAccount().getCurrency();
    			InflowOutflow inflowOutflow = getOrCreateInflowOutflow(interval, currency, locale, data);
    			process(transaction, inflowOutflow);
    		}
    	}
    	return data.values();
    }

    protected final InflowOutflow getOrCreateInflowOutflow(final Interval interval, final String currency, final Locale locale, final Map<String, InflowOutflow> inflowsOutflows) {
    	InflowOutflow inflowOutflow = inflowsOutflows.get(currency);
    	if (inflowOutflow == null) {
    		inflowOutflow = new InflowOutflow(interval, currency, locale, messageSource);
    		inflowsOutflows.put(currency, inflowOutflow);
    	}
    	return inflowOutflow;
    }

    protected final void process(final AccountTransaction transaction, final InflowOutflow inflowOutflow) {
    	boolean income = transaction.getSubcategory().isIncome();
    	Map<Category, Map<Subcategory, BigDecimal>> data = income ? inflowOutflow.getInflow(transaction.getOperationDate()) : inflowOutflow.getOutflow(transaction.getOperationDate());
    	Map<Subcategory, BigDecimal> categoryData = getOrCreateCategoryData(transaction, data);
    	BigDecimal amount = getOrCreateSubcategoryAmount(transaction, categoryData);
    	amount = amount.abs().add(transaction.getAmount().abs());
    	categoryData.put(transaction.getSubcategory(), amount);
    }

    protected final Map<Subcategory, BigDecimal> getOrCreateCategoryData(final AccountTransaction transaction, final Map<Category, Map<Subcategory, BigDecimal>> data) {
    	Subcategory subcategory = transaction.getSubcategory();
    	Category category = subcategory.getParentCategory();
    	Map<Subcategory, BigDecimal> categoryData = data.get(category);
    	if (categoryData == null) {
    		categoryData = new HashMap<Subcategory, BigDecimal>();
    		data.put(category, categoryData);
    	}
    	return categoryData;
    }

    protected BigDecimal getOrCreateSubcategoryAmount(final AccountTransaction transaction, final Map<Subcategory, BigDecimal> categoryData) {
    	Subcategory subcategory = transaction.getSubcategory();
    	BigDecimal amount = categoryData.get(subcategory);
    	if (amount == null) {
    		amount = BigDecimal.ZERO;
    		categoryData.put(subcategory, amount);
    	}
    	return amount;
    }

    @RequestMapping("categories")
    public String expensesByCategoryOverTime(final ModelMap modelMap) {
    	UserDetails user = getCurrentUser();
    	modelMap.addAttribute("subcategories", new TreeSet<Subcategory>(Subcategory.findExpenseCategories(user)));
    	return "reporting/categories";
    }

    @RequestMapping("categories-chart/{id}/{intervals}")
    public String expensesByCategoryOverTimeChartData(@PathVariable final Long id, @PathVariable final String intervals, final ModelMap modelMap) {
    	UserDetails user = getCurrentUser();
    	Interval interval = getInterval(intervals);
    	Subcategory subcat = Subcategory.findSubcategory(id);
    	Map<String, BigDecimal> maxValues = cache.getMaxCategoryData(user, subcat, intervals);
    	Map<String, Map<Date, NameValuePair<Date, BigDecimal>>> budgetData = cache.getAlloted(user, subcat, intervals);
    	Map<String, Map<Date, NameValuePair<Date, BigDecimal>>> currencyData = cache.getCategoryData(user, subcat, intervals);
    	if (maxValues == null) {
    		maxValues = new HashMap<String, BigDecimal>();
    		budgetData = new HashMap<String, Map<Date, NameValuePair<Date, BigDecimal>>>();
    		currencyData = new HashMap<String, Map<Date, NameValuePair<Date, BigDecimal>>>();
	    	for (AccountTransaction transaction : getTransactions(interval, subcat, modelMap)) {
	    		Locale locale = transaction.getAccount().getLocale();
	    		String isoCode = Currency.getInstance(locale).getCurrencyCode();
	    		if (!currencyData.containsKey(isoCode)) {
	    			maxValues.put(isoCode, BigDecimal.ZERO);
	    			currencyData.put(isoCode, createDateValueMap(interval));
	    			budgetData.put(isoCode, budget(user, subcat, locale, interval));
	    		}
	    		Date date = DateUtils.getMonthEndDate(transaction.getOperationDate());
	    		if (!currencyData.get(isoCode).containsKey(date)) {
	    			date = DateUtils.end(Calendar.getInstance()).getTime();
	    		}
	    		NameValuePair<Date, BigDecimal> stored = currencyData.get(isoCode).get(date);
	    		stored.setValue(stored.getValue().add(transaction.getAmount().abs()));
	    		maxValues.put(isoCode, maxValues.get(isoCode).max(stored.getValue()));
	    	}
	    	cache.storeAlloted(user, subcat, intervals, budgetData);
	    	cache.storeCategoryData(user, subcat, intervals, currencyData);
	    	cache.storeMaxCategoryData(user, subcat, intervals, maxValues);
    	}
    	modelMap.addAttribute("max", maxValues);
    	modelMap.addAttribute("category", subcat);
    	modelMap.addAttribute("currencies", currencyData.keySet());
    	for (String code : currencyData.keySet()) {
    		modelMap.addAttribute("data" + code, new TreeSet<NameValuePair<Date, BigDecimal>>(currencyData.get(code).values()));
    		modelMap.addAttribute("budget" + code, new TreeSet<NameValuePair<Date, BigDecimal>>(budgetData.get(code).values()));
    	}
    	if (widgetController != null) {
    		widgetController.incomeVsExpensesOverTime(interval.getNumberOfMonths(), modelMap);
    	}
    	return "reporting/categories-chart";
    }

    private Map<Date, NameValuePair<Date, BigDecimal>> createDateValueMap(Interval interval) {
    	Map<Date, NameValuePair<Date, BigDecimal>> data = new TreeMap<Date, NameValuePair<Date, BigDecimal>>();
    	for (Date date : DateUtils.dates(interval.getNumberOfMonths())) {
    		data.put(date, new NameValuePair<Date, BigDecimal>(date, BigDecimal.ZERO));
    	}
    	return data;
    }

    protected Map<Date, NameValuePair<Date, BigDecimal>> budget(UserDetails user, Subcategory category, Locale locale, Interval interval) {
    	Map<Date, NameValuePair<Date, BigDecimal>> budgetData = new HashMap<Date, NameValuePair<Date, BigDecimal>>();
    	Budget budget = user.getBudget();
    	for (Date date : DateUtils.dates(interval.getNumberOfMonths())) {
    		budgetData.put(date, new NameValuePair<Date, BigDecimal>(date, budget.getAlloted(date, locale, category)));
    	}
    	return budgetData;
    }
}
